/*
 * graph2D
 * Copyright (c) 2009 Shun Moriya <shun@mnu.sakura.ne.jp>
 *
 * The MIT License
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "data.h"
#include "graphicDevice.h"
#include "texture.h"
#include <cstring>
#include <fstream>

#if GRAPH2D_LOG_DETAIL_LEVEL
#include <libmana.h>
#endif

namespace Graph2D
{
	const char Texture::PVRIdentifier[4] = { 'P', 'V', 'R', '!' };
	GLuint Texture::lastBindTextureName = 0;

	////////////////////////////////////////////////////////////////////////////////////////////////////
	Texture::Texture() : name(0), numSurfaces(0), referenceCount(0)
	{
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	Texture::~Texture()
	{
		unload();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	void Texture::serialize(mana_stream* stream) const
	{
		((void)stream);
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	void Texture::deserialize(mana_stream* stream)
	{
		((void)stream);
		unload();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	bool Texture::load(const std::string& filename)
	{
		return load(filename.c_str());
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	bool Texture::load(const void* buffer)
	{
		const Header& header = *reinterpret_cast<const Header*>(buffer);
		if(std::memcmp(PVRIdentifier, &header.tag, sizeof(header.tag)) != 0)
			return false;

		GLuint formatFlags = header.flags & PVRTC_FLAG_TYPE_MASK;
		GLenum type        = 0;
		switch(formatFlags)
		{
#if defined(TARGET_IPHONE)
			case PVRTC_FLAG_TYPE_RGBA4444: format = GL_RGBA; type = GL_UNSIGNED_SHORT_4_4_4_4; break;
			case PVRTC_FLAG_TYPE_RGBA1555: format = GL_RGBA; type = GL_UNSIGNED_SHORT_5_5_5_1; break;
			case PVRTC_FLAG_TYPE_RGBA8888: format = GL_RGBA; type = GL_UNSIGNED_BYTE; break;
			case PVRTC_FLAG_TYPE_RGB565:   format = GL_RGB; type = GL_UNSIGNED_SHORT_5_6_5; break;
#endif
			case PVRTC_FLAG_TYPE_RGB888:   format = GL_RGB; type = GL_UNSIGNED_BYTE; break;
			case PVRTC_FLAG_TYPE_I8:       format = GL_LUMINANCE; type = GL_UNSIGNED_BYTE; break;
			case PVRTC_FLAG_TYPE_AI88:     format = GL_LUMINANCE_ALPHA; type = GL_UNSIGNED_BYTE; break;
#if defined(TARGET_IPHONE)
			case PVRTC_FLAG_TYPE_PVRTC_2:  format = GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG; MANA_ASSERT(header.width == header.height); break;
			case PVRTC_FLAG_TYPE_PVRTC_4:  format = GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG; MANA_ASSERT(header.width == header.height); break;
#endif
			case PVRTC_FLAG_TYPE_A8:       format = GL_ALPHA; type = GL_UNSIGNED_BYTE; break;
			default:
				return false;
		}

		width       = header.width;
		height      = header.height;
		alpha       = header.bitmaskAlpha ? true : false;
		numSurfaces = 0;

		GLuint w      = width;
		GLuint h      = height;
		GLuint offset = 0;
		const unsigned char* bytes = reinterpret_cast<const unsigned char*>((&header) + 1);
		while(offset < header.dataSize && numSurfaces < PVRTC_MAX_SURFACES)
		{
			GLuint blockSize, widthBlocks, heightBlocks;
			Surface& surface = surfaces[numSurfaces++];

			switch(formatFlags)
			{
				case PVRTC_FLAG_TYPE_RGBA4444:
				case PVRTC_FLAG_TYPE_RGBA1555:
				case PVRTC_FLAG_TYPE_RGBA8888:
				case PVRTC_FLAG_TYPE_RGB565:
				case PVRTC_FLAG_TYPE_RGB888:
				case PVRTC_FLAG_TYPE_I8:
				case PVRTC_FLAG_TYPE_AI88:
				case PVRTC_FLAG_TYPE_A8:
					blockSize = 1;
					widthBlocks = w;
					heightBlocks = h;
					break;
				case PVRTC_FLAG_TYPE_PVRTC_2:
					blockSize    = 8 * 4;
					widthBlocks  = w / 8;
					heightBlocks = h / 4;
					break;
				case PVRTC_FLAG_TYPE_PVRTC_4:
					blockSize    = 4 * 4;
					widthBlocks  = w / 4;
					heightBlocks = h / 4;
					break;
			}

			if(widthBlocks < 2)
				widthBlocks = 2;
			if(heightBlocks < 2)
				heightBlocks = 2;

			surface.size = widthBlocks * heightBlocks * ((blockSize * header.bpp) / 8);
			surface.bits = &bytes[offset];
			w >>= 1;
			if(w <= 0)
				w = 1;
			h >>= 1;
			if(h <= 0)
				h = 1;
			offset += surface.size;
		}

		GRAPH2D_CALL_GL(glGenTextures(1, &name));
		bind();

		w = width;
		h = height;
		for(GLuint i = 0 ; i < numSurfaces; ++i)
		{
			const Surface& surface = surfaces[i];
			if(type)
			{
				GRAPH2D_CALL_GL(glTexImage2D(
											 GL_TEXTURE_2D,
											 i,
											 format,
											 w,
											 h,
											 0,
											 format,
											 type,
											 surface.bits
											 ));
			}
			else
			{
#if defined(TARGET_IPHONE)
				GRAPH2D_CALL_GL(glCompressedTexImage2D(
													   GL_TEXTURE_2D,
													   i,
													   format,
													   w,
													   h,
													   0,
													   surface.size,
													   surface.bits
													   ));
#endif
			}
			w >>= 1;
			if(w <= 0)
				w = 1;
			h >>= 1;
			if(h <= 0)
				h = 1;
		}

		// テクスチャパラメータを設定
		GRAPH2D_CALL_GL(glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE));
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
		// GL_CLAMP_TO_EDGE
		// GL_REPEAT
#if 1
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
#else
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
		GRAPH2D_CALL_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
#endif

		// 参照カウンターを加算
		++referenceCount;

		// 読み込み成功
		return true;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	bool Texture::load(const char* filename)
	{
		bool result = false;

		unload();

		// データを読み込んでからテクスチャーを転送します
		{
			Data data(filename, true);
			if(data.valid())
			{
				// LZSSデータなら展開する
				if(mana_lzss_is_valid(data.getBuffer()))
				{
					const void* compressedData = data.getBuffer();
					const size_t extractBufferSize = mana_lzss_get_extract_data_size(compressedData);
					void* extractBuffer = malloc(extractBufferSize);
					if(extractBuffer)
					{
						mana_lzss_extract(extractBuffer, compressedData);

						result = load(extractBuffer);

						free(extractBuffer);
					}
				}
				else
				{
					result = load(data.getBuffer());
				}
			}
		}

#if GRAPH2D_LOG_DETAIL_LEVEL >= 1
		MANA_TRACE("Texture::load: %s ", filename);
		if(result)
			MANA_TRACE("#%d ... OK\n", name);
		else
			MANA_TRACE("... NG\n");
#endif

		return result;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	void Texture::retain()
	{
		// 参照カウンターを加算
		++referenceCount;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	GLushort Texture::unload()
	{
		// 参照カウンターを減らす
		if(referenceCount)
		{
			--referenceCount;
			if(referenceCount == 0)
			{

				if(name)
				{
#if GRAPH2D_LOG_DETAIL_LEVEL >= 1
					MANA_TRACE("Texture::unload: #%d ... ", name);
#endif
					if(lastBindTextureName == name)
						lastBindTextureName = 0;

					glDeleteTextures(1, &name);
					name = 0;
				}
			}
		}

		return referenceCount;
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	void Texture::bind()
	{
		if(lastBindTextureName != name)
		{
			GraphicDevice::flush();

			glBindTexture(GL_TEXTURE_2D, name);
			lastBindTextureName = name;
		}
	}

	////////////////////////////////////////////////////////////////////////////////////////////////////
	void Texture::unbind()
	{
		if(lastBindTextureName != 0)
		{
			GraphicDevice::flush();

			lastBindTextureName = 0;
		}
	}
}
